<?php

namespace PhpYes\Com;

use PhpYes\Yes;

class Router
{
    // route config
    // [
    //     [
    //         'ns' => '\\Dsdn\\Controller\\',
    //         'route' => [
    //             'GET /' => 'IndexController@index',
    //             'GET /test' => 'TestController@test',
    //         ]
    //     ],
    //     [
    //         'ns' => '\\Dsdn\\Controller\\',
    //         'route' => [
    //             'GET /docs' => 'DocsController@docs',
    //         ],
    //         'wild_route' => [
    //             ['req_meth' => 'GET', 'type' => '*', 'expr' => '/docs/*', 'cls' => 'DocsController', 'func' => 'entry'],
    //             ['req_meth' => 'GET', 'type' => '*', 'expr' => '/_docs_/*/*.js', 'cls' => 'DocsController', 'func' => 'entry_js']
    //         ]
    //     ]
    // ];

    /**
     * trim 过的 且 全部字母转成大写的 为路由准备的 request method
     * = strtoupper(trim($request_method));
     * @var string
     */
    public $request_method;

    /**
     * trim 过的，默认 / , 为路由准备的 path
     * = trim($path);
     * @var string
     */
    public $path;

    /**
     * 路由过程中获取到的信息存放到这里，
     * 将 class 和 function 单独提取出来，这样就可以很方便的判断路由是否成功了
     * 可能出现的字段如下 :
     * capture : array - wildcard 匹配成功时就会存在这个字段
     * @var array
     */
    public $route_info;

    /**
     * class
     * @var string
     */
    public $cls;

    /**
     * function
     * @var string
     */
    public $func;

    /**
     * 路由不成功时不要抛出异常来指示路由失败，抛出异常会将程序执行流程转到异常处理处，这样就让重写变得更加麻烦。
     * 不论这个 Router 的能力大小如何，其能力总是有边界的，总有可能会遇到其处理不了的情况，
     * 当遇到这些少见的，奇奇怪怪的，又处理不了的情况时，重写就是必须的，不论重写以怎样的形式出现(例如子类 或者 是另外单独的代码逻辑)，
     * 只要我们能判断出这里路由失败了，然后将程序执行流程转到我们重写的特殊路由处即可。
     * 虽然抛出异常将程序执行流程转到异常处理处后我们也能将程序执行流程再转到我们重写的特殊路由处，但是这样一来就让事情变得更加麻烦了。
     *
     * 以前设计的是 路由不成功时抛出 404 HttpException 来指示路由失败，在没有遇到需要重写时，这样做没啥大问题，
     * 但是遇到需要重写时，这样做就让事情变得麻烦了一些，而这些重写的特殊路由又不通用，不适合封装进这个 Router 中来，
     * 重写又有点麻烦，所以后来我们就决定改造这个 Router，让这个 Router 路由不成功时不要抛出异常来指示路由失败。
     *
     * route : map
     * "{$request_method} {$path}" => $handler
     * $path => $handler
     *
     * wild_route : ? *
     * ? : 匹配任意单个字符
     * * : 匹配任意字符串，但不包括空串，不允许出现 2 个连续的 *, 否则将被判定成无效，
     * 如果遇到需要 * 匹配空串的情况，那就再写一条 路由，例如:
     * /x*yy/zz , 如果需要 * 也能匹配空串，那就写成 /xyy/zz 和 /x*yy/zz
     *
     * @param string $request_method
     * @param string $path
     * @throws HttpException
     * @throws \Exception
     */
    public function route($request_method, $path)
    {
        $this->request_method = null;
        $this->path = null;
        $this->route_info = null;
        $this->cls = null;
        $this->func = null;

        $request_method = empty($request_method) ? '' : trim($request_method);
        if (empty($request_method)) {
            return $this;
        }
        $request_method = strtoupper($request_method);
        $this->request_method = $request_method;

        $path = empty($path) ? '' : trim($path);
        if (empty($path)) {
            $path = '/';
        }
        $this->path = $path;

        $config = Yes::$p->config_route();
        if (empty($config)) {
            return $this;
        }

        $route_key = "{$request_method} {$path}";
        $wild_routes = [];
        foreach ($config as $group) {
            if (!empty($group['wild_route'])) {
                $wild_routes[] = $group;
            }

            if (empty($group['route'])) {
                continue;
            }
            $route = $group['route'];

            $handler = null;
            if (isset($route[$route_key])) {
                $handler = $route[$route_key];
            } elseif (isset($route[$path])) {
                $handler = $route[$path];
            } else {
                continue;
            }
            $this->cls_func($handler, $group);
            if ($this->cls && $this->func) {
                return $this;
            }
        }

        if ($wild_routes) {
            $this->wild_route($wild_routes);
            if ($this->cls && $this->func) {
                return $this;
            }
        }

        $this->route_info = null;
        return $this;
    }

    /**
     * @param string $handler
     * @param array $group
     * @return $this
     */
    protected function cls_func($handler, $group)
    {
        if (empty($handler) || empty($group)) {
            return $this;
        }

        $idx = strrpos($handler, '@');
        if ($idx === false || $idx < 1 || $idx >= strlen($handler) - 1) {
            return $this;
        }
        $cls = substr($handler, 0, $idx);
        $this->func = substr($handler, $idx + 1);
        if ($cls[0] === '\\') {
            $this->cls = $cls;
        } else {
            $this->cls = empty($group['ns']) ? $cls : $group['ns'] . $cls;
        }
        return $this;
    }

    /**
     * @param array $wild
     * @param array $group
     * @return $this
     */
    protected function wild_cls_func($wild, $group)
    {
        if (empty($group) || empty($wild['cls']) || empty($wild['func'])) {
            return $this;
        }

        $cls = $wild['cls'];
        if ($cls[0] === '\\') {
            $this->cls = $cls;
        } else {
            $this->cls = empty($group['ns']) ? $cls : $group['ns'] . $cls;
        }
        $this->func = $wild['func'];
        return $this;
    }

    /**
     * @param array $groups
     * @return $this
     */
    protected function wild_route($groups)
    {
        if (empty($groups)) {
            return $this;
        }

        $request_method = $this->request_method;
        $path = $this->path;
        $path_len = strlen($path);
        foreach ($groups as $group) {
            if (empty($group['wild_route'])) {
                continue;
            }
            $wilds = $group['wild_route'];
            foreach ($wilds as $wild) {
                if (empty($wild['type']) || empty($wild['expr']) || empty($wild['cls']) || empty($wild['func'])) {
                    continue;
                }
                if (!(empty($wild['req_meth']) || $wild['req_meth'] === $request_method)) {
                    continue;
                }
                $type = $wild['type'];
                if (!($type === '*' || $type === '?')) {
                    continue;
                }

                // question
                if ($type === '?') {
                    $expr = $wild['expr'];
                    if ($path_len != strlen($expr)) {
                        continue;
                    }

                    // $is_ok == true;
                    $i = 0;
                    $capture = [];
                    for ($i = 0; $i < $path_len; $i++) {
                        if ($expr[$i] === '?') {
                            $capture[] = $path[$i];
                        } elseif ($expr[$i] !== $path[$i]) {
                            break;
                        }
                    }
                    if ($i < $path_len) {
                        continue;
                    }

                    $this->route_info = ['capture' => $capture];
                    $this->wild_cls_func($wild, $group);
                    return $this;
                }

                // asterisk
                $expr = $wild['expr'];
                $expr_len = strlen($expr);
                if ($path_len < $expr_len) {
                    continue;
                }
                // tokenize
                $tokens = [$expr[0]];
                $i = 1;
                $idx = 0;
                for ($i = 1; $i < $expr_len; $i++) {
                    $char = $expr[$i];
                    if ($char === '*') {
                        if ($tokens[$idx] === '*') {
                            break;
                        }
                        $tokens[] = '*';
                        $idx += 1;
                    } elseif ($tokens[$idx] === '*') {
                        $tokens[] = $char;
                        $idx += 1;
                    } else {
                        $tokens[$idx] .= $char;
                    }
                }
                if ($i < $expr_len) {
                    continue;
                }

                $tokens_len = count($tokens);
                if ($tokens_len == 1) {
                    $token = $tokens[0];
                    if ($token === '*') {
                        $this->route_info = ['capture' => [$path]];
                        $this->wild_cls_func($wild, $group);
                        return $this;
                    } elseif ($token === $path) {
                        $this->wild_cls_func($wild, $group);
                        return $this;
                    }

                    continue;
                } elseif ($tokens_len == 2) {
                    $token = $tokens[0];
                    if ($token === '*') {
                        $token = $tokens[1];
                        $len = strlen($token);
                        if ($token !== substr($path, -$len)) {
                            continue;
                        }
                        $this->route_info = ['capture' => [substr($path, 0, $path_len - $len)]];
                        $this->wild_cls_func($wild, $group);
                        return $this;
                    } else {
                        $len = strlen($token);
                        if ($token !== substr($path, 0, $len)) {
                            continue;
                        }
                        $this->route_info = ['capture' => [substr($path, $len)]];
                        $this->wild_cls_func($wild, $group);
                        return $this;
                    }

                    continue;
                }

                $token = $tokens[0];
                $path_idx = 0;
                if ($token === '*') {
                    $path_idx = 1; // * 匹配任意字符串，但不包括空串，所以 * 至少要匹配 1个 字符
                } else {
                    $len = strlen($token);
                    if ($token === substr($path, 0, $len)) {
                        $path_idx = $len;
                    } else {
                        continue;
                    }
                }

                $tokens_end = $tokens_len - 1;
                $token = $tokens[$tokens_end];
                $sub_path_len = 0;
                if ($token === '*') {
                    $sub_path_len = $path_len - 1;
                } else {
                    $len = strlen($token);
                    if ($token === substr($path, -$len)) {
                        $sub_path_len = $path_len - $len;
                    } else {
                        continue;
                    }
                }
                $match = array_fill(0, $tokens_len, 0);
                $match[$tokens_end] = $sub_path_len;
                $sub_path = substr($path, 0, $sub_path_len);

                for ($i = 1; $i < $tokens_end; $i++) {
                    if ($path_idx >= $sub_path_len) {
                        break;
                    }
                    $token = $tokens[$i];
                    if ($token === '*') {
                        $match[$i] = $path_idx;
                        $path_idx += 1;
                    } else {
                        $idx = strpos($sub_path, $token, $path_idx);
                        if (empty($idx)) { // false 或 0 都不行，因为 $path_idx > 0
                            break;
                        }
                        $match[$i] = $idx;
                        $path_idx = $idx + strlen($token);
                    }
                }
                if ($i < $tokens_end) {
                    continue;
                }

                $capture = [];
                if ($tokens[0] === '*') {
                    $capture[] = substr($path, 0, $match[1]);
                }
                for ($i = 1; $i < $tokens_end; $i++) {
                    if ($tokens[$i] === '*') {
                        $idx = $i - 1;
                        $idx = $match[$idx] + strlen($tokens[$idx]);
                        $len = $match[$i + 1] - $idx;
                        $capture[] = substr($path, $idx, $len);
                    }
                }
                if ($tokens[$tokens_end] === '*') {
                    $idx = $tokens_end - 1;
                    $capture[] = substr($path, $match[$idx] + strlen($tokens[$idx]));
                }
                $this->route_info = ['capture' => $capture];
                $this->wild_cls_func($wild, $group);
                return $this;
            }
        }
        return $this;
    }

}